package aseCrypto;

import java.math.BigInteger;
import java.util.Map.Entry;
import java.util.NavigableMap;
import java.util.Objects;
import java.util.TreeMap;
import java.util.Random;

/**
 *
 * Class AseInteger implements the most important methods of BigInteger and adds
 * the algorithms of our semester as methods BigInteger is used as a delegate
 * for the implementation of the BigInteger methods, so that the return values
 * and parameters are all AseIntegers
 *
 */
public class AseInteger implements Comparable<AseInteger> {

    public static final AseInteger ZERO = new AseInteger(BigInteger.ZERO);
    public static final AseInteger ONE = new AseInteger(BigInteger.ONE);
    public static final AseInteger TEN = new AseInteger(BigInteger.TEN);
    private BigInteger myValue;

    private BigInteger getMyValue() {
        return myValue;
    }

    private void setMyValue(BigInteger myValue) {
        this.myValue = myValue;
    }

    public AseInteger(BigInteger val) {
        setMyValue(val);
    }

    public AseInteger(byte[] val) {
        setMyValue(new BigInteger(val));
    }

    public AseInteger(int signum, byte[] magnitude) {
        setMyValue(new BigInteger(signum, magnitude));
    }

    public AseInteger(int bitLength, int certainity, Random rnd) {
        setMyValue(new BigInteger(bitLength, certainity, rnd));
    }

    public AseInteger(int numBits, Random rnd) {
        setMyValue(new BigInteger(numBits, rnd));
    }

    public AseInteger(String val, int radix) {
        setMyValue(new BigInteger(val, radix));
    }

    public AseInteger(String val) {
        setMyValue(new BigInteger(val));
    }

    public AseInteger add(AseInteger arg0) {
        return new AseInteger(this.getMyValue().add(arg0.getMyValue()));
    }

    @Override
    public int compareTo(AseInteger arg0) {
        return this.getMyValue().compareTo(arg0.getMyValue());
    }

    public AseInteger divide(AseInteger arg0) {
        return new AseInteger(this.getMyValue().divide(arg0.getMyValue()));
    }

    public AseInteger[] divideAndRemainder(AseInteger arg0) {
        BigInteger[] res = this.getMyValue().divideAndRemainder(arg0.getMyValue());
        AseInteger[] ret = new AseInteger[res.length];
        for (int i = 0; i < res.length; i++) {
            ret[i] = new AseInteger(res[i]);
        }
        return ret;
    }

    public AseInteger multiply(AseInteger arg0) {
        return new AseInteger(this.getMyValue().multiply(arg0.getMyValue()));
    }

    public AseInteger subtract(AseInteger arg0) {
        return new AseInteger(this.getMyValue().subtract(arg0.getMyValue()));
    }

    public AseInteger nextProbablePrime() {
        return new AseInteger(this.getMyValue().nextProbablePrime());
    }

    public AseInteger pow(int exponent) {
        return new AseInteger(this.getMyValue().pow(exponent));
    }

    public AseInteger setBit(int position) {
        return new AseInteger(this.getMyValue().setBit(position));
    }

    public String toString() {
        return this.getMyValue().toString();
    }

    /**
     * getGcd() returns the gcd of the current and another numbers using the
     * Euclidean Algorithm
     *
     * @param b The other number, the gcd of this and b is calculated
     * @return An AseInteger holding the gcd.
     */
    public AseInteger getGcd(AseInteger b) {

        AseInteger a = this;
        AseInteger q, r;

        // make sure that a is the dividend and b the divisor
        if (this.compareTo(b) < 0) { // if a < b, then just switch a and b
            a = b;
            b = this;
        }

        // iterate over all steps of the algorithm
        do {
            // a=bq+r
            AseInteger[] res = a.divideAndRemainder(b);
            q = res[0];
            r = res[1];

            // output the numbers in a=bq+r format to the screen, so that the user can view the algorithm running step by step
            System.out.println(a + "=" + b + "*" + q + "+" + r);

            // b becomes your new a, r becomes your new b
            a = b; // prepare the next row of the algorithm
            b = r;

            // iterate until no remainder is left
        } while (r.compareTo(AseInteger.ZERO) != 0);

        // the gcd is the b of the last division - which was already moved to a
        return a;
    }

    /**
     * getBezout() returns the factors x and y of bezouts equation as well as
     * the gcd for the two integers this and b
     *
     * @param secondNumber The other integer, the bezout factors x and y are
     * calculated for this (x) and b (y)
     * @return An array of three AseInteger holding x (element 0) and y (element
     * 1) and the gcd (element 2)
     */
    public AseInteger[] getBezout(AseInteger secondNumber) {

        AseInteger[] returnArray = new AseInteger[3];

        AseInteger biggerNumber = this;
        AseInteger quotient;
        AseInteger remainder;

        AseInteger x = new AseInteger("0");
        AseInteger y = new AseInteger("1");
        AseInteger lastX = new AseInteger("1");
        AseInteger lastY = new AseInteger("0");
        AseInteger m;
        AseInteger n;

        // make sure that a is the dividend and b the divisor
        if (this.compareTo(secondNumber) < 0) { // if a < b, then just switch a and b
            biggerNumber = secondNumber;
            secondNumber = this;
        }

        // iterate over all steps of the algorithm
        do {
            // a=bq+r
            AseInteger[] divisionResult = biggerNumber.divideAndRemainder(secondNumber);
            quotient = divisionResult[0];
            remainder = divisionResult[1];

            m = lastY.subtract(quotient.multiply(y));
            n = lastX.subtract(quotient.multiply(x));

            lastY = y;
            lastX = x;

            y = m;
            x = n;

            // output the numbers in a=bq+r format to the screen, so that the user can view the algorithm running step by step            
            //System.out.println(biggerNumber + "=" + b + "*" + quotient + "+" + remainder);
            // b becomes your new a, r becomes your new b
            biggerNumber = secondNumber; // prepare the next row of the algorithm
            secondNumber = remainder;

            // iterate until no remainder is left
        } while (remainder.compareTo(AseInteger.ZERO) != 0);

        // the gcd is the b of the last division - which was already moved to a
        returnArray[0] = lastY;
        //System.out.println( lastX );
        returnArray[1] = lastX;
        //System.out.println( lastY );
        returnArray[2] = biggerNumber;
        //System.out.println( biggerNumber );

        return returnArray;

    }

    /**
     * getGcdRec() returns the gcd of the current and another numbers using the
     * Euclidean Algorithm in recursive fashion
     *
     * @param b The other number, the gcd of this and b is calculated
     * @return An AseInteger holding the gcd.
     */
    public AseInteger getGcdRec(AseInteger b) {
        AseInteger a = this;

        // make sure that a is the dividend and b the divisor
        if (this.compareTo(b) < 0) { // if a < b, then just switch a and b
            a = b;
            b = this;
        }

        // a=bq+r
        AseInteger[] res = a.divideAndRemainder(b);

        // are we finished? (remainder == zero)
        if (res[1].compareTo(AseInteger.ZERO) == 0) {

            // then return the last divisor
            return b;
        } else {

            // otherwise return gcd(b,r) which we know to be equal to gcd(a,b)
            return b.getGcdRec(res[1]);
        }
    }

    /**
     * getBezoutRec() returns the factors x and y of bezouts equation as well as
     * the gcd for the two integers this and b in recursive fashion
     *
     * @param b The other integer, the bezout factors x and y are calculated for
     * this (x) and b (y)
     * @return An array of three AseInteger holding x (element 0) and y (element
     * 1) and the gcd (element 2)
     */
    public AseInteger[] getBezoutRec(AseInteger secondNumber) {
                
        AseInteger x;        
        AseInteger y;

        AseInteger biggerNumber = this;
        AseInteger[] returnArray;

        if (secondNumber.compareTo(ZERO) == 0) {

            returnArray = new AseInteger[3];
            returnArray[0] = ZERO;
            returnArray[1] = ONE;
            returnArray[2] = this;

            return returnArray;
        }

        AseInteger[] internalResult = secondNumber.getBezoutRec(biggerNumber.divideAndRemainder(secondNumber)[1]);

        x = internalResult[1].subtract(((this.divide(secondNumber)).multiply(internalResult[0])));
        y = internalResult[0];

        returnArray = new AseInteger[3];
        
        returnArray[0] = x;
        //System.out.println("Recursive 0: " + returnArray[0] );
        returnArray[1] = y;
        //System.out.println("Recursive 1: " + returnArray[1] );        
        returnArray[2] = internalResult[2];
        //System.out.println("Recursive 2: " + returnArray[2] );        
        
        return returnArray;
        
    }

    public AseInteger getGcdFact(AseInteger b) {
        AseInteger a = this;
        NavigableMap<AseInteger, Integer> aPrimeFactors = a.getPrimeFactors();
        NavigableMap<AseInteger, Integer> bPrimeFactors = b.getPrimeFactors();
        AseInteger gcd = AseInteger.ONE;

        Entry<AseInteger, Integer> entry = aPrimeFactors.firstEntry();
        do {
            Integer exponentB = bPrimeFactors.get(entry.getKey());
            AseInteger prime = entry.getKey();
            Integer exponentA = entry.getValue();

            if (exponentB != null) {
                if (Integer.compare(exponentA, exponentB) < 0) {
                    gcd = gcd.multiply(prime.pow(exponentA));
                    System.out.println("prime: " + prime + " exponent: " + exponentA);
                } else {
                    gcd = gcd.multiply(prime.pow(exponentB));
                    System.out.println("prime: " + prime + " exponent: " + exponentB);
                }
            }
            entry = aPrimeFactors.higherEntry(prime);
        } while (entry != null);

        return gcd;
    }

    public java.util.NavigableMap<AseInteger, Integer> getPrimeFactors() {
        NavigableMap<AseInteger, Integer> retmap = new TreeMap<>();
        AseInteger number = this;
        AseInteger prime = new AseInteger("2");
        Integer exponent = 0;

        while (number.compareTo(AseInteger.ONE) != 0) {
            AseInteger[] result = number.divideAndRemainder(prime);
            AseInteger quotient = result[0];
            AseInteger remainder = result[1];
            if (remainder.compareTo(AseInteger.ZERO) == 0) { // prime did divide number
                number = quotient;
                exponent = new Integer(exponent.intValue() + 1);
                retmap.put(prime, exponent);
            } else {
                exponent = new Integer(0);
                prime = prime.nextProbablePrime();
            }
        }
        System.out.println("Retmap" + retmap);
        return retmap;
    }

    /**
     * Implements the equals method for the class.
     *
     */
    @Override
    public boolean equals(Object obj) {
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        final AseInteger other = (AseInteger) obj;
        if (!Objects.equals(this.myValue, other.myValue)) {
            return false;
        }
        return true;
    }
}
